<script lang="ts">
	import CollapsedLane from '$components/CollapsedLane.svelte';
	import CreateBranchModal from '$components/CreateBranchModal.svelte';
	import MultiStackOfflaneDropzone from '$components/MultiStackOfflaneDropzone.svelte';
	import MultiStackPagination, { scrollToLane } from '$components/MultiStackPagination.svelte';
	import Scrollbar from '$components/Scrollbar.svelte';
	import StackDraft from '$components/StackDraft.svelte';
	import StackView from '$components/StackView.svelte';
	import { HorizontalPanner } from '$lib/dragging/horizontalPanner';
	import {
		onReorderEnd,
		onReorderMouseDown,
		onReorderStart,
		onDragOver
	} from '$lib/dragging/reordering';
	import { WorkspaceAutoPanner } from '$lib/dragging/workspaceAutoPanner';
	import { branchesPath } from '$lib/routes/routes.svelte';
	import { type SelectionId } from '$lib/selection/key';
	import { STACK_SERVICE } from '$lib/stacks/stackService.svelte';
	import { UI_STATE } from '$lib/state/uiState.svelte';
	import { throttle } from '$lib/utils/misc';
	import { inject } from '@gitbutler/core/context';
	import { persisted } from '@gitbutler/shared/persisted';
	import { DRAG_STATE_SERVICE } from '@gitbutler/ui/drag/dragStateService.svelte';
	import { resizeObserver } from '@gitbutler/ui/utils/resizeObserver';
	import { isDefined } from '@gitbutler/ui/utils/typeguards';
	import { flip } from 'svelte/animate';
	import type { Stack } from '$lib/stacks/stack';

	type Props = {
		projectId: string;
		stacks: Stack[];
		selectionId: SelectionId;
		scrollToStackId?: string;
		onScrollComplete?: () => void;
	};

	let { projectId, stacks, scrollToStackId, onScrollComplete }: Props = $props();

	const uiState = inject(UI_STATE);
	const stackService = inject(STACK_SERVICE);
	const dragStateService = inject(DRAG_STATE_SERVICE);

	// Persisted folded stacks state per project (without expiration)
	const foldedStacks = persisted<string[]>([], `folded-stacks-${projectId}`);

	let lanesScrollableEl = $state<HTMLDivElement>();
	let lanesScrollableWidth = $state<number>(0);
	let lanesScrollableHeight = $state<number>(0);
	let stackElements = $state<Record<string, HTMLElement>>({});

	let laneWidths = $state<number[]>([]);
	let lineHights = $state<number[]>([]);
	let isNotEnoughHorzSpace = $derived(
		(lanesScrollableWidth ?? 0) < laneWidths.length * (laneWidths[0] ?? 0)
	);
	let visibleIndexes = $state<number[]>([0]);
	let isCreateNewVisible = $state<boolean>(false);

	/** Used to offset content shift from opening an unassigned change preview. */
	let lastWidth = $state<number>();

	let createBranchModal = $state<CreateBranchModal>();

	const projectState = $derived(uiState.project(projectId));
	const exclusiveAction = $derived(projectState.exclusiveAction.current);
	const isDraftStackVisible = $derived(
		(exclusiveAction?.type === 'commit' && exclusiveAction.stackId === undefined) ||
			exclusiveAction?.type === 'codegen'
	);

	const SHOW_PAGINATION_THRESHOLD = 1;

	// Throttle calls to the reordering code in order to save some cpu cycles.
	const throttledDragOver = throttle(onDragOver, 25);

	// To support visual reordering of stacks we need a copy of the array
	// that can be mutated as the stack is being dragged around.
	let mutableStacks = $state<Stack[]>([]);

	// Enable panning when a stack is being dragged.
	let draggingStack = $state(false);

	// This is a bit of anti-pattern, and reordering should be better
	// encapsulated such that we don't need this somewhat messy code.
	$effect(() => {
		if (stacks) {
			mutableStacks = stacks;
		}
	});

	const workspaceAutoPanner = $derived(
		lanesScrollableEl ? new WorkspaceAutoPanner(lanesScrollableEl) : undefined
	);

	// Enable panning when anything is being dragged.
	const isDragging = dragStateService.isDragging;
	$effect(() => {
		if ($isDragging || draggingStack) {
			const unsub = workspaceAutoPanner?.enablePanning();
			return () => unsub?.();
		}
	});

	const horizontalPanner = $derived(
		lanesScrollableEl ? new HorizontalPanner(lanesScrollableEl) : undefined
	);

	$effect(() => {
		if (horizontalPanner) {
			const unsub = horizontalPanner.registerListeners();
			return () => unsub?.();
		}
	});

	// Scroll to stack when scrollToStackId is set
	$effect(() => {
		if (scrollToStackId && stacks.length > 0 && lanesScrollableEl) {
			setTimeout(() => {
				const stackEl = stackElements[scrollToStackId];
				if (stackEl) {
					stackEl.scrollIntoView({ behavior: 'smooth' });
				}
				onScrollComplete?.();
			}, 50);
		}
	});
</script>

{#if isNotEnoughHorzSpace}
	<div class="pagination-container">
		<MultiStackPagination
			length={stacks.length}
			{visibleIndexes}
			{isCreateNewVisible}
			selectedBranchIndex={stacks.findIndex(() => {
				return false;
				// TODO: Integrate this with new focusables
				// return s.id === focusedStackId;
			})}
			onPageClick={(index) => scrollToLane(lanesScrollableEl, index)}
			onCreateNewClick={() => {
				scrollToLane(lanesScrollableEl, stacks.length + 1);
			}}
		/>
	</div>
{/if}

<div
	class="scrollbar-container hide-native-scrollbar"
	role="presentation"
	bind:this={lanesScrollableEl}
	bind:clientWidth={lanesScrollableWidth}
	bind:clientHeight={lanesScrollableHeight}
	class:multi={stacks.length < SHOW_PAGINATION_THRESHOLD}
	ondrop={() => {
		stackService.updateStackOrder({
			projectId,
			stacks: mutableStacks
				.map((b, i) => (b.id ? { id: b.id, order: i } : undefined))
				.filter(isDefined)
		});
	}}
	use:resizeObserver={(data) => {
		// An experiment in prevent content shift. Currently this mechanism
		// allows content shift if the main viewport is scrolled to the left,
		// because the content shift is most annoying when there are many
		// lanes and you are e.g. making a commit to one of them.
		if (lastWidth && lanesScrollableEl) {
			// Only offset for sudden large changes.
			const diff = lastWidth - lanesScrollableEl.clientWidth;
			if (Math.abs(diff) > 300 && lanesScrollableEl.scrollLeft > 300) {
				lanesScrollableEl.scrollBy({ left: diff });
			}
		}
		lastWidth = data.frame.width;
	}}
>
	<div class="lanes-scrollable">
		<StackDraft
			{projectId}
			visible={isDraftStackVisible}
			mode={exclusiveAction?.type === 'codegen' ? 'codegen' : 'commit'}
		/>

		<!--
	Ideally we wouldn't key on stack id, but the opacity change is done on the
	element being dragged, and therefore stays in place without the key. We
	should find a way of encapsulating the reordering logic better, perhaps
	with some feedback that enables the opacity to become a prop for
	`StackView` instead of being set imperatively in the dragstart handler.
	 -->
		{#each mutableStacks as stack, i (stack.id)}
			<div
				bind:this={stackElements[stack.id || 'branchless']}
				class="reorderable-stack dotted-pattern"
				role="presentation"
				animate:flip={{ duration: draggingStack ? 200 : 0 }}
				onmousedown={onReorderMouseDown}
				ondragstart={(e) => {
					if (!stack.id) return;
					const isCollapsedLane = $foldedStacks.includes(stack.id);
					onReorderStart(
						e,
						stack.id,
						() => {
							draggingStack = true;
						},
						isCollapsedLane
					);
				}}
				ondragover={(e) => {
					if (!stack.id) return;
					throttledDragOver(e, mutableStacks, stack.id);
				}}
				ondragend={() => {
					draggingStack = false;
					onReorderEnd();
				}}
			>
				{#if stack.id && $foldedStacks.includes(stack.id)}
					<CollapsedLane
						stackId={stack.id}
						branchNames={stack.heads.map((head) => head.name)}
						{projectId}
					/>
				{:else}
					<StackView
						{projectId}
						laneId={stack.id || 'banana'}
						stackId={stack.id ?? undefined}
						topBranchName={stack.heads.at(0)?.name}
						bind:clientWidth={laneWidths[i]}
						bind:clientHeight={lineHights[i]}
						onVisible={(visible) => {
							if (visible) {
								visibleIndexes = [...visibleIndexes, i];
							} else {
								visibleIndexes = visibleIndexes.filter((index) => index !== i);
							}
						}}
					/>
				{/if}
			</div>
		{/each}

		<MultiStackOfflaneDropzone
			{projectId}
			viewport={lanesScrollableEl}
			onVisible={(visible) => {
				isCreateNewVisible = visible;
			}}
		>
			{#snippet title()}
				{#if stacks.length === 0}
					No branches in Workspace
				{/if}
			{/snippet}
			{#snippet description()}
				{#if stacks.length === 0}
					Drop files to start a branch,
					<br />
					apply from the
					<a
						class="pointer-events underline-dotted clr-text-2 link-hover-2"
						aria-label="Branches view"
						href={branchesPath(projectId)}>branches view</a
					>
					↗
					<br />
					or
					<button
						type="button"
						class="underline-dotted pointer-events clr-text-2 link-hover-2"
						onclick={() => createBranchModal?.show()}>create a new branch</button
					> +
				{:else}
					Drop files to start a branch,
					<br />
					or
					<button
						type="button"
						class="underline-dotted pointer-events clr-text-2 link-hover-2"
						onclick={() => createBranchModal?.show()}>create a new branch</button
					> +
				{/if}
			{/snippet}
		</MultiStackOfflaneDropzone>

		<div class="dotted-pattern"></div>
	</div>

	{#if lanesScrollableEl}
		<Scrollbar viewport={lanesScrollableEl} horz />
	{/if}
</div>

<CreateBranchModal bind:this={createBranchModal} {projectId} />

<style lang="postcss">
	.scrollbar-container {
		display: flex;
		flex: 1;
		height: 100%;
		margin-right: -1px; /* to hide vertical lane border gap */
		overflow-x: auto;
		overflow-y: hidden;
	}

	.lanes-scrollable {
		display: flex;
		position: relative;
		min-width: stretch;
		height: 100%;
	}

	.dotted-pattern {
		width: stretch;
		height: 100%;
	}

	.pagination-container {
		display: flex;
		z-index: var(--z-floating);
		position: absolute;
		right: 6px;
		bottom: 8px;
	}

	.reorderable-stack {
		display: flex;
		z-index: var(--z-ground);
		flex-shrink: 0;
		flex-direction: column;
		width: fit-content;
		height: 100%;
		margin-left: -1px;
		border-left: 1px solid var(--clr-border-2);
		background-color: var(--clr-bg-2);
	}
</style>
